Intention.js is a lightweight tool for responsive design developed at Dow Jones that manipulates the DOM via HTML attributes. The methods for manipulation are placed with the elements themselves, so flexible layouts don't have to be so abstract and messy.
What should an element's classes be on mobile vs tablet? Where should advertising markup be placed when viewed on a desktop browser? Does the page require an alternate slideshow widget on touch-enabled devices? These are all scenarios that Intention.js can handle, altering the page based onusers' devices. Context.js creates a set of common page contexts for width thresholds, touch devices, high-res displays and a fallback.
And you can easily add your own contexts on top of these, or create all your own custom threshold group.
Let's start with the basics, though.
Thresholds, Axes, & Contexts
Intention.js works by determining what are called “threshold groups”. These threshold groups define the context in which a user is viewing your site. Threshold groups work by measuring an axis, like the browser’s width or the device’s pixel density (for high resolution screens).
A lot of these contexts are predictable, and some common patterns are included in a handy implementation of intention.js called context.js
Axis
- context_name, value
width (a window of at least x pixels wide)
- mobile, 0
- tablet, 510
- standard, 840
orientation (degree of window.rotation)
- portrait, 0
- landscape, 90
touch (window.ontouchstart boolean)
- touch, true
highres (devicePixelRatio > 1)
- highres, true
Manipulations via HTML Attributes
Intention.js has three basic manipulations: attribution manipulations, class manipulations, and placement manipulations. With these three, you can change the value of any attribute, add or remove an element’s class, and adjust the position of an element within the structure of the document.
Usage
Basic Syntax
Giving Intention.js instructions is as easy as flagging the element as "intentional" and giving it an intentional attribute.
Intentional attributes can be specific to:
A change in contexts within an axis
A specific context passing true
A specific context within a specific axis passing true
For the purposes of documentation, in- will be used instead of the proper HTML-valid data-in-.
Dependencies
Intention.js requires jQuery and Underscore.js to work. You can download and link to them manually, or you can include them via require.
Compatibility
Intention.js is tested to work on all modern browsers, including Internet Explorer back to IE8! Woo-hoo!
Note: jQuery 2.x dropped support for IE8, so obviously using it in conjunction with Intention will not work in IE8. If you don't care about that, rest assured there is nothing Intention needs in jQuery 1.x that isn't available in jQuery 2.x.
Intention Markup
On an iPhone 5
On Regular Tablets
On Desktops
time.respond('');
Intention.js responds to a bunch of device contexts, but it’s open to any index. You can create custom contexts based on anything you can measure, and restructure your code in response!
It’s not just changing a page based on the browser’s width: it’s noticing touch-capabilities, portrait/landscape orientation, high resolution contexts. Intention.js can be taught to restructure pages based on scroll depth, pageviews, time of day—basically anything!
Manipulations
Class Manipulations
The simplest intentional attribute is a class manipulation. This manipulation adds the current context as a class to the element. Adding in-axis_name: (note the trailing colon) to a flagged element is enough to get it working.
↓ ↓ ↓
Intention does not touch attributes that are assigned outside of in- commands — so
will keep its class foo regardless of what horizontal_axis context is passed.
Attribute Manipulation
Intention.js can also manipulate an element's attributes with more specificity than can be achieved via class manipulations. To start, set a base (default) attribute in case no contexts are met, then specify context-specific attribute values.
↓ ↓ ↓
Attribute manipulation can used for more specific class manipulations, too.
Placement Manipulation
Intention.js can rearrange elements within a page layout based on the context.
Suppose we want to demote the status of the navigation when the user is on smaller devices. The following specification on the navigation might do what we need:
When the device is 320px wide or less, the navigation will sit at the top of the footer. When the device is between 321px and 768px wide, it will appear right below the section. Obviously, on larger displays (wider than 769px) the navigation will be at the end of the header.
Move functions
Intention.js provides four basic functions for rearranging elements. They include:
These function just like jQuery DOM manipulations.
Selectors can be general element selections like above (...prepend="footer"), or specific (...prepend="#intro").
One of intention.js's most exciting features is its scalability: you can use it to respond to almost anything! Any type of data that is quantifiable can be used to manipulate the page. Of course context.js comes with the most common patterns of responsive design, but let's take a look at how to create our own.
Markup
Each axis is made up of four basic properties: the axis ID (optional), the context group, the matcher function, and the measure function,. As we know, an axis is a measurable object or set of information. This axis is optionally given an ID so it can be used in specific manipulations. A measure function finds the current measurement of that axis, and the matcher function finds where that measurement lies in a set of thresholds and breakpoints called contexts. Contexts are defined in an ordered array and help dictate when the DOM should be manipulated.
First things first
If you are completely abandoning context.js, you must be sure to first create an Intention object. This is required for any axis creation or response. Context.js does this right out of the box, so if you are only extending the included axes, you need not create a new Intention object. Be sure to check out the next section Initialization to see what else context.js takes care of—things you'll have to do when starting from scratch.
Contexts
To work with whatever data we measure, we need to set up contexts that will act as thresholds. The contexts property is an ordered array of objects. Each context object represents a range of data. If a measurement falls in that range of data, the corresponding context passes true. Then that context is set as the current context.
When a measurement is being matched against the contexts, the function will iterate through the array in order, so the breakpoints must be listed in an increasing or decreasing order. If a context passes true, the array is immediately exited.
Each context object must have a name property. This is used to identify exactly what context is true. Be sure the context's name is a string!
Measure Function
The measure function's task is simply to find data that will later be matched against the contexts. It can be a series of complex operations, but as long as it returns a value, it's doing its job. In most cases, this function is just a return that passes off a value to the matcher function.
Matcher Function
The matcher function uses the measure function's returned value and the context array as parameters. It tests the measurement against each context's value property. When a measurement fits in a context's data range, the context passes true. This is done with a comparative statement. For example, if a measurement does not exceed the maximum value of a context, then the context will pass true. If it does, then the matcher function will see if exceeds the next context's maximum value (which will be a higher value).
The comparative statement must agree with the order of the contexts. If context values are listed in descending order, the matcher function must test if the measurement is greater than or equal to the context minimum value. If it is, then we know it definitely is greater than all of the other contexts' minimum values.
Axis IDs
An axis ID property allows for context-aware restructuring. Although this property is optional, it is used in HTML attributes to command DOM manipulations. An axis ID lets you create manipulations for any change in contexts in a specified axis, or for any specific context within a specific axis.
Without an assigned ID, the axis is randomly given a hash as an ID that changes on every page load—not very helpful for making specific changes to the layout.
Putting it all together
Responding
Every intent.responsive axis returns some useful properties, of which respond is probably the most important. This property contains a function that sets off the measure and matcher functions. Calling axis.respond() updates the current context within an axis.
Note that the intent.responsive({...})'s variable name is used to for responding, not the axis ID. Keep variable scope in mind! You can always use intent.axes.axisID.respond() if you are out of the axis variable's scope.
If your axis needs only respond once, you can make it do so right after you create the axis with a trailing respond command. The touch axis is such an axis: Intention needs only test touch capabilities at page load because they are unlikely to change mid-session.
Finding the Current Context
Another useful property intent.responsive returns is current. Calling this property will return the name of the most recent context passed as a string.
Intialization
If you're not using context.js, there's a few things you need to do to get Intention working. Context.js does this for you, but you may find yourself using intention.js for something other than standard responses.
Finding Elements
Once all the contexts have been written, Intention must search the DOM for elements that have been flagged "intentional". It will find each element and create a record of it and its manipulations in an array of objects intent.elms. Every specified manipulation will be saved in this array as instructions, so when an axis or context is passed the manipulation is more immediate.
To perform this search, first construct all of your axes and threshold groups, then run this function at doc ready.
Should you need to inject intentional HTML after the page load, you can always add elements to this registry using intent.add().
For demonstration purposes, here's an example of an object saved in the element array.
Context.js returns the Intentional object right out of the box, so you can use it across plugins and files; but if you are just using intention.js and your own custom axes, you might want to also extend the Intentional object yourself.
Allowing the Intentional object to be used in plugins and other files is as simple as saving it to a global variable. At the end of your document, simply create a variable for the window scope that matches the variable name for the Intention object you created in the very beginning.
Intent Events
It's possible (and very easy) to set up event listeners for context changes. Intention.js supports event binding to scenarios such as:
When a specific context passes true
When a specific context in a specific axis passes true
When a specific axis passes any context
The syntax generally follows jQuery's syntax for event binding. Preface the .on() event handler with the namespace intent.
The event itself is made up of two optional parts: an axis ID and a context name. The axis ID refers to the axis' ID property and should always be followed by a : . Using the axis ID on its own will fire every time a context in that axis is passed. Specifying a context name will narrow the event to fire only when that context within that axis is passed.
Alternatively, an event handler can be created for contexts without a specified axis. Simply excluding the axis ID component (and its trailing :) will make a more general event listener for the specified context name. In this scenario, be careful of naming conflicts. If any two contexts share the same name, both will fire this same event.
Below is a list of reference IDs for the axes supplied in context.js
axis: ID
- contexts
horizontal_axis: width
- standard
- tablet
- mobile
orientation_axis: orientation
- portrait
- landscape
touch: touch
highres: highres
1. When a specific context passed true.
2. When a specific context in an specific axis passes true
3. When a specific axis passes any context
Note: currently, Intention does not support multiple event types per event handler. You will have to chain events instead.
Event Handlers for Intention's First Response
There are cases when you need to listen for Intention's first response on page load—maybe to insert specific content or run certain functions. There are obviously a number of ways to do this, but here are two simple methods.
One-time Page Load Responses
On certain occasions you may only care about the first context passed when the page first loads. After that first load, you may not want the event handler to keep firing. In this scenario, we can use jQuery's Deferred Objects.
The above function init() accepts two parameters: an array of context names to be passed and a callback function that is triggered when the array is satisfied. With supplied each context, init() creates a deferred object and adds it to a master array.
If the user is already in that context when the function is called, the deferred object will be resolved. If the user is not already in that specific context, init will create an event handler to wait for the context to pass true, when it will resolve the deferred object. When all of the deferred objects in the master array have been resolved, the callback function will fire once and only once.
Repeating Event Handlers
If you do not care about the first response on page load exclusively but want to create an event handler that still affects the first response, the easiest way is to edit context.js. We want to have all event handlers be written before the affected axes respond. Otherwise, the first manipulations will have already occurred before the event handler is even created.
About
Intention.js was developed by Joe Kendall at the Dow Jones Consumer Technology Group, with contributions from Erin Sparling, Tyler Paige, Adrian Lafond, and Mike Stamm. Major contributions to documentation and examples were provided by Tyler Paige, Camila Mercado and Paul Pangrazzi.
MIT license for everything
Copyright (c) 2012 The Wall Street Journal,
http://wsj.com/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.